#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Test the functionality of the Intel IFS(In Field Scan) driver.
#
# It's from Linux Kernel v6.11-rc1 kself-tests:
# tools/testing/selftests/drivers/platform/x86/intel/ifs/test_ifs.sh
#
# Original Author: Pengfei Xu <pengfei.xu@intel.com>
#

# Matched with kselftest framework: tools/testing/selftests/kselftest.h
readonly KSFT_PASS=0
readonly KSFT_FAIL=1
readonly KSFT_XFAIL=2
readonly KSFT_SKIP=4

readonly CPU_SYSFS="/sys/devices/system/cpu"
readonly CPU_OFFLINE_SYSFS="${CPU_SYSFS}/offline"
readonly IMG_PATH="/lib/firmware/intel/ifs_0"
readonly IFS_SCAN_MODE="0"
readonly IFS_ARRAY_BIST_SCAN_MODE="1"
readonly IFS_PATH="/sys/devices/virtual/misc/intel_ifs"
readonly IFS_SCAN_SYSFS_PATH="${IFS_PATH}_${IFS_SCAN_MODE}"
readonly IFS_ARRAY_BIST_SYSFS_PATH="${IFS_PATH}_${IFS_ARRAY_BIST_SCAN_MODE}"
readonly RUN_TEST="run_test"
readonly STATUS="status"
readonly DETAILS="details"
readonly STATUS_PASS="pass"
readonly PASS="PASS"
readonly FAIL="FAIL"
readonly INFO="INFO"
readonly XFAIL="XFAIL"
readonly SKIP="SKIP"
readonly IFS_NAME="intel_ifs"
readonly ALL="all"
readonly SIBLINGS="siblings"

# Matches arch/x86/include/asm/intel-family.h and
# drivers/platform/x86/intel/ifs/core.c requirement as follows
readonly SAPPHIRERAPIDS_X="8f"
readonly EMERALDRAPIDS_X="cf"

readonly INTEL_FAM6="06"

LOOP_TIMES=3
FML=""
MODEL=""
STEPPING=""
CPU_FMS=""
TRUE="true"
FALSE="false"
RESULT=$KSFT_PASS
IMAGE_NAME=""
INTERVAL_TIME=1
OFFLINE_CPUS=""
# For IFS cleanup tags
ORIGIN_IFS_LOADED=""
IFS_IMAGE_NEED_RESTORE=$FALSE
IFS_LOG="/tmp/ifs_logs.$$"
RANDOM_CPU=""
DEFAULT_IMG_ID=""

append_log()
{
  echo -e "$1" | tee -a "$IFS_LOG"
}

online_offline_cpu_list()
{
  local on_off=$1
  local target_cpus=$2
  local cpu=""
  local cpu_start=""
  local cpu_end=""
  local i=""

  if [[ -n "$target_cpus" ]]; then
    for cpu in $(echo "$target_cpus" | tr ',' ' '); do
      if [[ "$cpu" == *"-"* ]]; then
        cpu_start=""
        cpu_end=""
        i=""
        cpu_start=$(echo "$cpu" | cut -d "-" -f 1)
        cpu_end=$(echo "$cpu" | cut -d "-" -f 2)
        for((i=cpu_start;i<=cpu_end;i++)); do
          append_log "[$INFO] echo $on_off > \
${CPU_SYSFS}/cpu${i}/online"
          echo "$on_off" > "$CPU_SYSFS"/cpu"$i"/online
        done
      else
        set_target_cpu "$on_off" "$cpu"
      fi
    done
  fi
}

ifs_scan_result_summary()
{
  local failed_info pass_num skip_num fail_num

  if [[ -e "$IFS_LOG" ]]; then
    failed_info=$(grep ^"\[${FAIL}\]" "$IFS_LOG")
    fail_num=$(grep -c ^"\[${FAIL}\]" "$IFS_LOG")
    skip_num=$(grep -c ^"\[${SKIP}\]" "$IFS_LOG")
    pass_num=$(grep -c ^"\[${PASS}\]" "$IFS_LOG")

    if [[ "$fail_num" -ne 0 ]]; then
      RESULT=$KSFT_FAIL
      echo "[$INFO] IFS test failure summary:"
      echo "$failed_info"
    elif [[ "$skip_num" -ne 0 ]]; then
      RESULT=$KSFT_SKIP
    fi
      echo "[$INFO] IFS test pass:$pass_num, skip:$skip_num, fail:$fail_num"
  else
    echo "[$INFO] No file $IFS_LOG for IFS scan summary"
  fi
}

ifs_cleanup()
{
  echo "[$INFO] Restore environment after IFS test"

  # Restore ifs origin image if origin image backup step is needed
  [[ "$IFS_IMAGE_NEED_RESTORE" == "$TRUE" ]] && {
    mv -f "$IMG_PATH"/"$IMAGE_NAME"_origin "$IMG_PATH"/"$IMAGE_NAME"
  }

  # Restore the CPUs to the state before testing
  [[ -z "$OFFLINE_CPUS" ]] || online_offline_cpu_list "0" "$OFFLINE_CPUS"

  lsmod | grep -q "$IFS_NAME" && [[ "$ORIGIN_IFS_LOADED" == "$FALSE" ]] && {
    echo "[$INFO] modprobe -r $IFS_NAME"
    modprobe -r "$IFS_NAME"
  }

  ifs_scan_result_summary
  [[ -e "$IFS_LOG" ]] && rm -rf "$IFS_LOG"

  echo "[RESULT] IFS test exit with $RESULT"
  exit "$RESULT"
}

do_cmd()
{
  local cmd=$*
  local ret=""

  append_log "[$INFO] $cmd"
  eval "$cmd"
  ret=$?
  if [[ $ret -ne 0 ]]; then
    append_log "[$FAIL] $cmd failed. Return code is $ret"
    RESULT=$KSFT_XFAIL
    ifs_cleanup
  fi
}

test_exit()
{
  local info=$1
  RESULT=$2

  declare -A EXIT_MAP
  EXIT_MAP[$KSFT_PASS]=$PASS
  EXIT_MAP[$KSFT_FAIL]=$FAIL
  EXIT_MAP[$KSFT_XFAIL]=$XFAIL
  EXIT_MAP[$KSFT_SKIP]=$SKIP

  append_log "[${EXIT_MAP[$RESULT]}] $info"
  ifs_cleanup
}

online_all_cpus()
{
  local off_cpus=""

  OFFLINE_CPUS=$(cat "$CPU_OFFLINE_SYSFS")
  online_offline_cpu_list "1" "$OFFLINE_CPUS"

  off_cpus=$(cat "$CPU_OFFLINE_SYSFS")
  if [[ -z "$off_cpus" ]]; then
    append_log "[$INFO] All CPUs are online."
  else
    append_log "[$XFAIL] There is offline cpu:$off_cpus after online all cpu!"
    RESULT=$KSFT_XFAIL
    ifs_cleanup
  fi
}

get_cpu_fms()
{
  FML=$(grep -m 1 "family" /proc/cpuinfo | awk -F ":" '{printf "%02x",$2;}')
  MODEL=$(grep -m 1 "model" /proc/cpuinfo | awk -F ":" '{printf "%02x",$2;}')
  STEPPING=$(grep -m 1 "stepping" /proc/cpuinfo | awk -F ":" '{printf "%02x",$2;}')
  CPU_FMS="${FML}-${MODEL}-${STEPPING}"
}

check_cpu_ifs_support_interval_time()
{
  get_cpu_fms

  if [[ "$FML" != "$INTEL_FAM6" ]]; then
    test_exit "CPU family:$FML does not support IFS" "$KSFT_SKIP"
  fi

  # Ucode has time interval requirement for IFS scan on same CPU as follows:
  case $MODEL in
    "$SAPPHIRERAPIDS_X")
      INTERVAL_TIME=180;
      ;;
    "$EMERALDRAPIDS_X")
      INTERVAL_TIME=30;
      ;;
    *)
      # Set default interval time for other platforms
      INTERVAL_TIME=1;
      append_log "[$INFO] CPU FML:$FML model:0x$MODEL, default: 1s interval time"
      ;;
  esac
}

check_ifs_loaded()
{
  local ifs_info=""

  ifs_info=$(lsmod | grep "$IFS_NAME")
  if [[ -z "$ifs_info" ]]; then
    append_log "[$INFO] modprobe $IFS_NAME"
    modprobe "$IFS_NAME" || {
      test_exit "Check if CONFIG_INTEL_IFS is set to m or \
platform doesn't support ifs" "$KSFT_SKIP"
    }
    ifs_info=$(lsmod | grep "$IFS_NAME")
    [[ -n "$ifs_info" ]] || test_exit "No ifs module listed by lsmod" "$KSFT_FAIL"
  fi
}

test_ifs_scan_entry()
{
  local ifs_info=""

  ifs_info=$(lsmod | grep "$IFS_NAME")

  if [[ -z "$ifs_info" ]]; then
    ORIGIN_IFS_LOADED="$FALSE"
    check_ifs_loaded
  else
    ORIGIN_IFS_LOADED="$TRUE"
    append_log "[$INFO] Module $IFS_NAME is already loaded"
  fi

  if [[ -d "$IFS_SCAN_SYSFS_PATH" ]]; then
    append_log "[$PASS] IFS sysfs $IFS_SCAN_SYSFS_PATH entry is created\n"
  else
    test_exit "No sysfs entry in $IFS_SCAN_SYSFS_PATH" "$KSFT_FAIL"
  fi
}

load_image()
{
  local image_id=$1
  local image_info=""
  local ret=""

  check_ifs_loaded
  if [[ -e "${IMG_PATH}/${IMAGE_NAME}" ]]; then
    append_log "[$INFO] echo 0x$image_id > ${IFS_SCAN_SYSFS_PATH}/current_batch"
    echo "0x$image_id" > "$IFS_SCAN_SYSFS_PATH"/current_batch 2>/dev/null
    ret=$?
    [[ "$ret" -eq 0 ]] || {
      append_log "[$FAIL] Load ifs image $image_id failed with ret:$ret\n"
      return "$ret"
    }
    image_info=$(cat ${IFS_SCAN_SYSFS_PATH}/current_batch)
    if [[ "$image_info" == 0x"$image_id" ]]; then
      append_log "[$PASS] load IFS current_batch:$image_info"
    else
      append_log "[$FAIL] current_batch:$image_info is not expected:$image_id"
      return "$KSFT_FAIL"
    fi
  else
    append_log "[$FAIL] No IFS image file ${IMG_PATH}/${IMAGE_NAME}"\
    return "$KSFT_FAIL"
  fi
  return 0
}

test_load_origin_ifs_image()
{
  local image_id=$1

  IMAGE_NAME="${CPU_FMS}-${image_id}.scan"

  load_image "$image_id" || return $?
  return 0
}

test_load_bad_ifs_image()
{
  local image_id=$1

  IMAGE_NAME="${CPU_FMS}-${image_id}.scan"

  do_cmd "mv -f ${IMG_PATH}/${IMAGE_NAME} ${IMG_PATH}/${IMAGE_NAME}_origin"

  # Set IFS_IMAGE_NEED_RESTORE to true before corrupt the origin ifs image file
  IFS_IMAGE_NEED_RESTORE=$TRUE
  do_cmd "dd if=/dev/urandom of=${IMG_PATH}/${IMAGE_NAME} bs=1K count=6 2>/dev/null"

  # Use the specified judgment for negative testing
  append_log "[$INFO] echo 0x$image_id > ${IFS_SCAN_SYSFS_PATH}/current_batch"
  echo "0x$image_id" > "$IFS_SCAN_SYSFS_PATH"/current_batch 2>/dev/null
  ret=$?
  if [[ "$ret" -ne 0 ]]; then
    append_log "[$PASS] Load invalid ifs image failed with ret:$ret not 0 as expected"
  else
    append_log "[$FAIL] Load invalid ifs image ret:$ret unexpectedly"
  fi

  do_cmd "mv -f ${IMG_PATH}/${IMAGE_NAME}_origin ${IMG_PATH}/${IMAGE_NAME}"
  IFS_IMAGE_NEED_RESTORE=$FALSE
}

test_bad_and_origin_ifs_image()
{
  local image_id=$1

  append_log "[$INFO] Test loading bad and then loading original IFS image:"
  test_load_origin_ifs_image "$image_id" || return $?
  test_load_bad_ifs_image "$image_id"
  # Load origin image again and make sure it's worked
  test_load_origin_ifs_image "$image_id" || return $?
  append_log "[$INFO] Loading invalid IFS image and then loading initial image passed.\n"
}

ifs_test_cpu()
{
  local ifs_mode=$1
  local cpu_num=$2
  local image_id status details ret result result_info

  echo "$cpu_num" > "$IFS_PATH"_"$ifs_mode"/"$RUN_TEST"
  ret=$?

  status=$(cat "${IFS_PATH}_${ifs_mode}/${STATUS}")
  details=$(cat "${IFS_PATH}_${ifs_mode}/${DETAILS}")

  if [[ "$ret" -eq 0 && "$status" == "$STATUS_PASS" ]]; then
    result="$PASS"
  else
    result="$FAIL"
  fi

  cpu_num=$(cat "${CPU_SYSFS}/cpu${cpu_num}/topology/thread_siblings_list")

  # There is no image file for IFS ARRAY BIST scan
  if [[ -e "${IFS_PATH}_${ifs_mode}/current_batch" ]]; then
    image_id=$(cat "${IFS_PATH}_${ifs_mode}/current_batch")
    result_info=$(printf "[%s] ifs_%1d cpu(s):%s, current_batch:0x%02x, \
ret:%2d, status:%s, details:0x%016x" \
           "$result" "$ifs_mode" "$cpu_num" "$image_id" "$ret" \
           "$status" "$details")
  else
    result_info=$(printf "[%s] ifs_%1d cpu(s):%s, ret:%2d, status:%s, details:0x%016x" \
           "$result" "$ifs_mode" "$cpu_num" "$ret" "$status" "$details")
  fi

  append_log "$result_info"
}

ifs_test_cpus()
{
  local cpus_type=$1
  local ifs_mode=$2
  local image_id=$3
  local cpu_max_num=""
  local cpu_num=""

  case "$cpus_type" in
    "$ALL")
      cpu_max_num=$(($(nproc) - 1))
      cpus=$(seq 0 $cpu_max_num)
      ;;
    "$SIBLINGS")
      cpus=$(cat ${CPU_SYSFS}/cpu*/topology/thread_siblings_list \
        | sed -e 's/,.*//' \
        | sed -e 's/-.*//' \
        | sort -n \
        | uniq)
      ;;
    *)
      test_exit "Invalid cpus_type:$cpus_type" "$KSFT_XFAIL"
      ;;
  esac

  for cpu_num in $cpus; do
    ifs_test_cpu "$ifs_mode" "$cpu_num"
  done

  if [[ -z "$image_id" ]]; then
    append_log "[$INFO] ifs_$ifs_mode test $cpus_type cpus completed\n"
  else
    append_log "[$INFO] ifs_$ifs_mode $cpus_type cpus with $CPU_FMS-$image_id.scan \
completed\n"
  fi
}

test_ifs_same_cpu_loop()
{
  local ifs_mode=$1
  local cpu_num=$2
  local loop_times=$3

  append_log "[$INFO] Test ifs mode $ifs_mode on CPU:$cpu_num for $loop_times rounds:"
  [[ "$ifs_mode" == "$IFS_SCAN_MODE" ]] && {
    load_image "$DEFAULT_IMG_ID" ||  return $?
  }
  for (( i=1; i<=loop_times; i++ )); do
    append_log "[$INFO] Loop iteration: $i in total of $loop_times"
    # Only IFS scan needs the interval time
    if [[ "$ifs_mode" == "$IFS_SCAN_MODE" ]]; then
      do_cmd "sleep $INTERVAL_TIME"
    elif [[ "$ifs_mode" == "$IFS_ARRAY_BIST_SCAN_MODE" ]]; then
      true
    else
      test_exit "Invalid ifs_mode:$ifs_mode" "$KSFT_XFAIL"
    fi

    ifs_test_cpu "$ifs_mode" "$cpu_num"
  done
  append_log "[$INFO] $loop_times rounds of ifs_$ifs_mode test on CPU:$cpu_num completed.\n"
}

test_ifs_scan_available_imgs()
{
  local image_ids=""
  local image_id=""

  append_log "[$INFO] Test ifs scan with available images:"
  image_ids=$(find "$IMG_PATH" -maxdepth 1 -name "${CPU_FMS}-[0-9a-fA-F][0-9a-fA-F].scan" \
        2>/dev/null \
        | sort \
        | awk -F "-" '{print $NF}' \
        | cut -d "." -f 1)

  for image_id in $image_ids; do
    load_image "$image_id" || return $?

    ifs_test_cpus "$SIBLINGS" "$IFS_SCAN_MODE" "$image_id"
    # IFS scan requires time interval for the scan on the same CPU
    do_cmd "sleep $INTERVAL_TIME"
  done
}

prepare_ifs_test_env()
{
  local max_cpu=""

  check_cpu_ifs_support_interval_time

  online_all_cpus
  max_cpu=$(($(nproc) - 1))
  RANDOM_CPU=$(shuf -i 0-$max_cpu -n 1)

  DEFAULT_IMG_ID=$(find $IMG_PATH -maxdepth 1 -name "${CPU_FMS}-[0-9a-fA-F][0-9a-fA-F].scan" \
       2>/dev/null \
       | sort \
       | head -n 1 \
       | awk -F "-" '{print $NF}' \
       | cut -d "." -f 1)
}

test_ifs()
{
  prepare_ifs_test_env

  test_ifs_scan_entry

  if [[ -z "$DEFAULT_IMG_ID" ]]; then
    append_log "[$SKIP] No proper ${IMG_PATH}/${CPU_FMS}-*.scan, skip ifs_0 scan"
  else
    test_bad_and_origin_ifs_image "$DEFAULT_IMG_ID"
    test_ifs_scan_available_imgs
    test_ifs_same_cpu_loop "$IFS_SCAN_MODE" "$RANDOM_CPU" "$LOOP_TIMES"
  fi

  if [[ -d "$IFS_ARRAY_BIST_SYSFS_PATH" ]]; then
    ifs_test_cpus "$SIBLINGS" "$IFS_ARRAY_BIST_SCAN_MODE"
    test_ifs_same_cpu_loop "$IFS_ARRAY_BIST_SCAN_MODE" "$RANDOM_CPU" "$LOOP_TIMES"
  else
    append_log "[$SKIP] No $IFS_ARRAY_BIST_SYSFS_PATH, skip IFS ARRAY BIST scan"
  fi
}

trap ifs_cleanup SIGTERM SIGINT
test_ifs
ifs_cleanup
